/**
 * 
 */
package org.rollinitiative.d20.entity;

import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Hashtable;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rollinitiative.d20.AdjustableValue;
import org.rollinitiative.d20.Adjustment;
import org.rollinitiative.d20.entity.Movement.EncumberanceType;
import org.rollinitiative.d20.entity.classes.ClassContentBridge;
import org.rollinitiative.d20.entity.skills.Skill;
import org.rollinitiative.d20.entity.skills.SkillsContentBridge;
import org.rollinitiative.d20.entity.talents.Talent;
import org.rollinitiative.d20.items.Item;
import org.rollinitiative.d20.items.ItemContentBridge;
import org.rollinitiative.d20.schema.character.AdvancementType;
import org.rollinitiative.d20.schema.character.CharacterType;
import org.rollinitiative.d20.schema.character.InventoryItemType;
import org.rollinitiative.d20.schema.d20.AbilitySetType;
import org.rollinitiative.d20.schema.skill.TrainingType;
import org.rollinitiative.d20.schema.talent.TalentType;

/**
 * The CharacterFactory is primarily responsible for producing Player objects from CharacterType XML
 * content.
 * 
 * @author bebopjmm
 * 
 */
public class CharacterFactory
{
    static final Log LOG = LogFactory.getLog(CharacterFactory.class);

    /**
     * Bridge to accessing Race-based content.
     */
    RaceContentBridge raceBridge;

    /**
     * Bridge to accessing Class-based content
     */
    ClassContentBridge classBridge;

    /**
     * Bridge to accessing Skill-based content
     */
    SkillsContentBridge skillBridge;

    ItemContentBridge itemBridge;


    /**
     * @param bridge
     */
    public void setRaceContentBridge(RaceContentBridge bridge)
    {
        raceBridge = bridge;
    }


    public RaceContentBridge getRaceContentBridge()
    {
        return raceBridge;
    }


    /**
     * @param bridge
     */
    public void setClassContentBridge(ClassContentBridge bridge)
    {
        classBridge = bridge;
    }


    public ClassContentBridge getClassContentBridge()
    {
        return classBridge;
    }


    /**
     * @return the skillBridge
     */
    public SkillsContentBridge getSkillBridge()
    {
        return skillBridge;
    }


    /**
     * @param skillBridge the skillBridge to set
     */
    public void setSkillBridge(SkillsContentBridge skillBridge)
    {
        this.skillBridge = skillBridge;
    }


    /**
     * @return the itemBridge
     */
    public ItemContentBridge getItemBridge()
    {
        return itemBridge;
    }


    /**
     * @param itemBridge the itemBridge to set
     */
    public void setItemBridge(ItemContentBridge itemBridge)
    {
        this.itemBridge = itemBridge;
    }


    /**
     * Instantiates a Player from the provided characterXML
     * 
     * @param characterXML
     * @return
     * @since incr-0.1
     */
    public Player load(CharacterType characterXML) throws InvalidEntityException
    {
        if (skillBridge == null) {
            LOG.error("SkillContentBridge has not been set!");
            return null; // this should be an exception
        }

        if (itemBridge == null) {
            LOG.error("ItemContentBridge has not been set!");
            return null; // this should be an exception
        }

        if (classBridge == null) {
            LOG.error("ClassContentBridge has not been set!");
            return null; // this should be an exception
        }

        if (raceBridge == null) {
            LOG.error("RaceContentBridge has not been set!");
            return null; // this should be an exception
        }

        LOG.info("Loading character name = " + characterXML.getName());
        Player newPlayer = new Player("Automagic");
        newPlayer.setName(characterXML.getName());
        if (characterXML.getPlayerName() != null) {
            newPlayer.setPlayerName(characterXML.getPlayerName());
        }

        // Configure abilities.
        AbilitySetType abilities = characterXML.getBaseAbilityScores();
        newPlayer.setAbilityBaseValue(Ability.STR, abilities.getSTR().intValue());
        newPlayer.setAbilityBaseValue(Ability.DEX, abilities.getDEX().intValue());
        newPlayer.setAbilityBaseValue(Ability.CON, abilities.getCON().intValue());
        newPlayer.setAbilityBaseValue(Ability.INT, abilities.getINT().intValue());
        newPlayer.setAbilityBaseValue(Ability.WIS, abilities.getWIS().intValue());
        newPlayer.setAbilityBaseValue(Ability.CHA, abilities.getCHA().intValue());
        if (LOG.isDebugEnabled()) {
            for (Ability a : EnumSet.range(Ability.STR, Ability.CHA)) {
                LOG.debug("Ability, " + a + " has base value: "
                        + newPlayer.abilities.get(a).getBase());
            }
        }

        // Configure Description
        newPlayer.setCurrentAlign(Alignment.mapAlignmentEnum(characterXML.getDescription()
                .xgetAlignment()));
        newPlayer
                .setCurrentGender(Gender.mapGenderEnum(characterXML.getDescription().xgetGender()));

        // Apply Racial Characteristics
        String raceID = characterXML.getRaceID();
        LOG.debug("Applying racial characteristics for raceID = " + raceID);
        try {
            Size raceSize = raceBridge.getSize(raceID);
            Hashtable<Ability, Adjustment> adjustments = raceBridge.getAbilityMods(raceID);
            newPlayer.setRace(raceID, raceSize, adjustments);
            for (Movement.EncumberanceType encumberance : EnumSet.range(EncumberanceType.LIGHT,
                    EncumberanceType.HEAVY)) {
                int speed = raceBridge.getSpeed(raceID, encumberance);
                newPlayer.setBaseMovement(encumberance, speed);
                LOG.debug("Base movement for encumberance: " + encumberance.toString() + " = "
                        + speed);
            }
        } catch (InvalidEntityException e) {
            LOG.error("Failed to locate race information for: " + raceID);
            throw e;
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("Abilities following racial assignment:");
            for (Ability a : EnumSet.range(Ability.STR, Ability.CHA)) {
                LOG.debug("Ability, " + a + " has current value: "
                        + newPlayer.abilities.get(a).getCurrent());
            }
        }
        LOG.debug("racial characteristics complete");

        int classSkillBonus = skillBridge.getClassSkillBonus();
        newPlayer.skills.setClassSkillBonus(classSkillBonus);

        // Establish the base set of skills (Everyone has untrained skills in their list)
        Skill[] untrainedSkills = skillBridge.retrieveUntrainedSkills();
        if (untrainedSkills != null) {
            LOG.info("Initializing basic skills, total = " + untrainedSkills.length);
            for (Skill skill : untrainedSkills) {
                newPlayer.initSkill(skill);
            }
        }
        else {
            LOG
                    .warn("NULL untrained skills! There is likely a problem with the SkillContentBridge!");
        }

        // 
        // Process the Level Advancements. For each level we need to
        // 1. Advance the correct class level.
        // 2. Advance the hit points
        // 3. Advance the skill points
        // 4. Advance abilities
        //
        newPlayer.setXp(characterXML.getAdvancement().getExperience().intValue());
        AdvancementType[] levels = characterXML.getAdvancement().getLevelEntryArray();
        String classID;
        Hashtable<String, Integer> classTable = new Hashtable<String, Integer>();
        newPlayer.setLevel(levels.length);
        LOG.info(characterXML.getName() + " is a level " + levels.length + " character");

        for (int i = 0; i < levels.length; i++) {
            classID = levels[i].getClassID();
            if (classTable.containsKey(classID)) {
                int level = classTable.get(classID).intValue() + 1;
                LOG.debug("Advancing " + characterXML.getName() + " level in " + classID + " to "
                        + level);
                classTable.put(classID, Integer.valueOf(level));
            }
            else {
                classTable.put(classID, Integer.valueOf(1));
                LOG.debug("Initializing " + characterXML.getName() + " level in " + classID
                        + " to 1");
            }

            // Aggregate Hit Points.
            newPlayer.levelUpHP(levels[i].getHp().intValue());

            // Aggregate Skills
            TrainingType[] skillList = levels[i].getSkillsLearned().getSkillTrainedArray();
            for (TrainingType trainingType : skillList) {
                String skillID = trainingType.getSkillID().getLocalPart();
                Skill skill = skillBridge.retrieveSkill(skillID);

                // // if new skill, then it must be initialized.
                // if (!newPlayer.hasSkill(skill)) {
                // newPlayer.initSkill(skill);
                // }
                // add the ranks to the skill
                newPlayer.advanceSkill(skill, trainingType.getPoints().intValue());
            }

            // Process any Class Talents for this level.
            processTalents(newPlayer, classBridge.getTalents(classID, i));
        }

        // Apply Racial skill bonuses for the character level.
        Hashtable<String, Adjustment> skillAdjustments = raceBridge.getSkillMods(raceID, newPlayer
                .getLevel());
        Set<String> skillIDs = skillAdjustments.keySet();
        for (String skillID : skillIDs) {
            Skill skill = skillBridge.retrieveSkill(skillID);
            // if new skill, then it must be initialized.
            if (!newPlayer.hasSkill(skill)) {
                newPlayer.initSkill(skill);
            }
            AdjustableValue skillValue = newPlayer.getSkill(skill);
            skillValue.addAdjustment(skillAdjustments.get(skillID));
        }

        // Establish Base Attack Bonus and Saving Throws based on class(es) and level.
        String[] classes = classTable.keySet().toArray(new String[classTable.keySet().size()]);
        if (classes.length == 1) {
            int level = classTable.get(classes[0]).intValue();
            String d20classID = classes[0];
            LOG.info(characterXML.getName() + " is a level " + level + " " + d20classID);
            try {
                newPlayer.setMaxBaseAttackBonus(classBridge.getBaseAttackBonus(d20classID, level));
                newPlayer.setClassDescript(classBridge.getDescriptiveNameShort(d20classID) + level);
                // Assign Class-Based Adjustments to Saving Throws
                Adjustment saveAdj = null;
                AbilityListenerValue saveVal = null;
                EnumMap<Save, Adjustment> saves = classBridge.getSaves(d20classID, level);
                for (Save save : Save.values()) {
                    saveAdj = saves.get(save);
                    saveVal = newPlayer.saves.get(save);
                    saveVal.addAdjustment(saveAdj);
                    LOG.debug("New save for " + save + " = " + saveVal.getCurrent());
                }
            } catch (InvalidEntityException e) {
                LOG.error("Failed to retrieve d20 class: " + d20classID, e);
                throw e;
            }
        }
        else {
            LOG.error("This character is multi-class, not supported.");
            return null;
        }

        // Process the character's inventory.
        InventoryItemType[] inventory = characterXML.getEquipment().getItemArray();
        processInventory(newPlayer, inventory);

        return newPlayer;
    }


    void processTalents(Actor actor, TalentType[] talents)
    {
        Talent talent = null;
        for (TalentType talentType : talents) {
            talent = new Talent();
            talent.setId(talentType.getId().toString());
            talent.setName(talentType.getName());
            talent.setDescription(talentType.getDescription());
            actor.addTalent(talent);
        }
    }


    /**
     * This method instantiates instances of each item in the InventoryItemType array and adds it to
     * the actor's inventory.
     * 
     * @param actor The actor to receive the inventory items
     * @param items Array of InventoryItemTypes
     * @throws InvalidEntityException when a specified InventoryItemType can't be located.
     */
    void processInventory(Actor actor, InventoryItemType[] items) throws InvalidEntityException
    {
        LOG.debug("Total Inventory Items to process = " + items.length);
        for (InventoryItemType inventoryItemType : items) {
            String id = inventoryItemType.getItemIDRef().getLocalPart();
            Item item = itemBridge.makeInstanceOf(id);
            if (item != null) {
                actor.inventory.addItem(item);
            }
        }
    }
}
